package nl.boektrust.btsoep.server;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;

import nl.boektrust.btsoep.shared.model.PhysicalBookManifestation;

import org.restlet.data.Status;
import org.restlet.resource.Get;  
import org.restlet.resource.ResourceException;
import org.restlet.resource.ServerResource;  
import org.restlet.routing.Extractor;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;

import com.googlecode.objectify.Key;

public class RestBooksResource extends ServerResource {
	private enum ResultSetType {full, basic, rest}; 
	
	private boolean isObjectifyServiceRegistered = false;
	PersistentServiceImpl persistentServiceImpl = new PersistentServiceImpl();


@Get 
public String retrieve() {
	HashMap<String,SearchParametersTuple> searchKeys = new HashMap<String,SearchParametersTuple>();
	for (int i=0; i<5; i++) {
		String key = (String) getRequest().getAttributes().get("key"+i);
		String value = (String) getRequest().getAttributes().get("value"+i);
		String flags = (String) getRequest().getAttributes().get("flags"+i);
		if (key != null && value != null && key.length() > 0 && value.length() > 0) {
			SearchParametersTuple tuple = new SearchParametersTuple(key, value, flags);
			searchKeys.put(key, tuple);

		}
	}
	String limitString = (String) getRequest().getAttributes().get("limit");
	int limit=-1;
	if (limitString != null) {
		limit = new Integer(limitString).intValue();
	} 

	ResultSetType resultSetType = ResultSetType.full;
	String resultSetTypeString =  (String) getRequest().getAttributes().get("resultSetType");
	String offsetString =  (String) getRequest().getAttributes().get("offset");
	if (null != resultSetTypeString) {
		resultSetType = ResultSetType.valueOf(resultSetTypeString);
	}
	System.err.println("RestBooksResource: extracted "+searchKeys.size()+" valid key/val pairs from query string");
	System.err.println("RestBooksResource: limit "+limit);
	System.err.println("RestBooksResource: resultSetTypeString "+resultSetTypeString);
	System.err.println("RestBooksResource: resultSetType "+resultSetType);
	System.err.println("RestBooksResource: offset "+offsetString);
	if (searchKeys.size() < 0 || searchKeys.size() > 4) {
		throw(new ResourceException(new Status(400), "at least 1, and at most 5, key and value pairs must be specified."));
	}
	/* search algorithm: first tuple fills up the recordReferences set. 
	 * Every subsequent tuple checks off those that aren't in that set. 
	 */
	HashSet <Key<PhysicalBookManifestation>>recordReferences = new HashSet<Key<PhysicalBookManifestation>>();
	int numTuplesUsed = 0;
	for (SearchParametersTuple tuple : searchKeys.values()) {
		HashSet <Key<PhysicalBookManifestation>>currentRecordReferences = new HashSet<Key<PhysicalBookManifestation>>();
		if (tuple.getKey().equals("recordreference")) {
			currentRecordReferences = keysByRecordReference(tuple);
		} else if (tuple.getKey().equals("title")) {
			currentRecordReferences = keysByTitle(tuple);
		} else if (tuple.getKey().equals("contributor")) {
			currentRecordReferences = keysByContributor(tuple);

		} else if (tuple.getKey().equals("geo")) {
			currentRecordReferences = keysByGeocode(tuple, limit);

		} else if (tuple.getKey().equals("nur")) {
			currentRecordReferences = keysByNURcode(tuple, limit);

		} else if (tuple.getKey().equals("imprint")) {
			currentRecordReferences = keysByImprint(tuple);

		} else if (tuple.getKey().equals("recent")) {
			currentRecordReferences = keysByRecent(tuple);
		} else {
			throw(new ResourceException(new Status(401), "invalid search key: "+tuple.getKey()));
		}
		System.out.println("tuple "+numTuplesUsed+": search returned "+currentRecordReferences.size()+" records");
		if (numTuplesUsed == 0) {
			recordReferences = currentRecordReferences;
		} else {
			for (Key<PhysicalBookManifestation> k : recordReferences) {
				if (!currentRecordReferences.contains(k)) {
					System.out.println("removing key "+k.getId());
					recordReferences.remove(k);
				}
			}
		}
		System.out.println("resulting recordReferences: "+ recordReferences.size()+" records");

		numTuplesUsed ++;
	}

	String resultString = "";
	switch (resultSetType) {
	case basic :
	{
		Map<Key<PhysicalBookManifestation>, PhysicalBookManifestation> books = persistentServiceImpl.getBooksByKeysInList(recordReferences);
		resultString = encodeToOnix(books.values());
	}
		break;
	case full :
	{
		Map<Key<PhysicalBookManifestation>, PhysicalBookManifestation> books = persistentServiceImpl.getBooksByKeysInList(recordReferences);
		resultString = encodeToOnix(books.values());
	}
		break;
	case rest : 
		resultString = encodeToRest(recordReferences);
		break;
	}
	System.out.println("resulting XML: "+resultString);

	
	/*
	 * this method:
	 * 1. create a list of recordreferences based on each of the key-value pairs;o k
	 * 2. take the common ones to mimic AND behavior; ok
	 * 3. retrieve the records from these common ones; ok
	 * 4. return the results. 
	 */
	
	return resultString;
}


private String encodeToRest(Collection<Key<PhysicalBookManifestation>> bookKeys) {
	String returnString = "";
	String baseURL = "http://btsoep.appspot.com/rest/bookByKey/";
	boolean first = true;
	for (Key<PhysicalBookManifestation> key : bookKeys) {
		if (first == true) {
			returnString = baseURL + key.getId();
			first = false;
		} else {
			returnString = returnString + " " + baseURL+ key.getId();
		}
	}
	return returnString;
}

private String encodeToOnix(Collection<PhysicalBookManifestation> books) {
	DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	Document doc = null;
	try {
		doc = factory.newDocumentBuilder().newDocument();
	} catch (Exception e) {
		System.err.println("Exception");
		e.printStackTrace();

		System.err.println(e.getMessage());
		System.err.println(e.getStackTrace());
		throw(new ResourceException(new Status(501), "encodeToOnix failed to allocate a Document resource, critical error."));
	}
	// 	encode the message
	DocumentFragment onixMessage = OnixUtility.getOnixMessage();
	if (onixMessage == null) {
		System.err.println("fragment is null");
		return "";
	}
	doc.appendChild(doc.importNode(onixMessage, true));
	if (onixMessage.getOwnerDocument() == null) {
		System.err.println("owner document is null");
		return "";
	}
	// add header
	DocumentFragment header = OnixUtility.getOnixMessageHeader();
	doc.getDocumentElement().appendChild(doc.importNode(header, true));
	for (PhysicalBookManifestation book : books) {
		// add product node for each product
		DocumentFragment product = OnixUtility.encodeToOnix(book);
		doc.getDocumentElement().appendChild(doc.importNode(product, true));
	}
	Transformer transformer;
	String xmlString = "";
	try {
		// see: http://comments.gmane.org/gmane.org.google.gwt/45531
		System.setProperty("javax.xml.transform.TransformerFactory", "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
		transformer = TransformerFactory.newInstance().newTransformer();
		java.io.StringWriter writer = new StringWriter();
		javax.xml.transform.stream.StreamResult result = new javax.xml.transform.stream.StreamResult(writer);
		DOMSource source = new DOMSource(doc);
		transformer.transform(source, result);
		xmlString = result.getWriter().toString();
		if (xmlString == null) System.out.println("xmlString is null");
	} catch (Exception e) {
		System.err.println("Exception thrown: " + e.getMessage());
		e.printStackTrace();
	}
	return xmlString;


}

private HashSet <Key<PhysicalBookManifestation>> keysByRecordReference(SearchParametersTuple tuple) {
	if (tuple.getValue() == null) return null;
	List<Key<PhysicalBookManifestation>> books = persistentServiceImpl.getBookKeysByRecordReference(new Long(tuple.getValue()));
	HashSet <Key<PhysicalBookManifestation>> returnSet = new HashSet <Key<PhysicalBookManifestation>>();
	for (Key<PhysicalBookManifestation> k : books) {
		returnSet.add(k);
	}
	return returnSet;
}

private HashSet <Key<PhysicalBookManifestation>> keysByTitle(SearchParametersTuple tuple) {
	if (tuple.getValue() == null) return null;
	HashSet <Key<PhysicalBookManifestation>> books = persistentServiceImpl.getBookKeysByTitle(tuple.getValue());
	HashSet <Key<PhysicalBookManifestation>> returnSet = new HashSet <Key<PhysicalBookManifestation>>();
	for (Key<PhysicalBookManifestation> k : books) {
		returnSet.add(k);
	}
	return returnSet;
}

private HashSet <Key<PhysicalBookManifestation>> keysByContributor(SearchParametersTuple tuple) {
	//TODO should add code here to verify that flags is actually set to a valid contributor.
	//TODO should add code in getBookKeysByContributor to actually do something with the specified contributor (now ignored)
	//TODO actually test this function
	HashSet <Key<PhysicalBookManifestation>> books = persistentServiceImpl.getBookKeysByContributor(tuple.getValue(), tuple.getFlags());
	HashSet <Key<PhysicalBookManifestation>> returnSet = new HashSet <Key<PhysicalBookManifestation>>();
	for (Key<PhysicalBookManifestation> k : books) {
		returnSet.add(k);
	}
	return returnSet;
}

private HashSet <Key<PhysicalBookManifestation>> keysByGeocode(SearchParametersTuple tuple, int limit) {
	int offset=0;
	HashSet <Key<PhysicalBookManifestation>> books = null;
	HashSet <Key<PhysicalBookManifestation>> returnSet = null;
	boolean sortOrder=true;
	if (tuple.getValue() != null) {
		books= persistentServiceImpl.getBookKeysByGeocode(tuple.getValue(), limit, offset, sortOrder);
		returnSet = new HashSet <Key<PhysicalBookManifestation>>();
		for (Key<PhysicalBookManifestation> k : books) {
			returnSet.add(k);
		}
	}
	return returnSet;
}
private HashSet <Key<PhysicalBookManifestation>> keysByImprint(SearchParametersTuple tuple) {
	if (tuple.getValue() == null) return null;
	return null;

}

 private boolean isRangeArgument(String st) {
	if (st != null && st.matches("^[0-9][0-9][0-9]-[0-9][0-9][0-9]$")) return true;
	return false;
}
 
 private boolean isSingleIntValueArgument(String st) {
		if (st != null && st.matches("^[0-9][0-9][0-9]$")) return true;
		return false;
	}

private HashSet <Key<PhysicalBookManifestation>> keysByNURcode(SearchParametersTuple tuple, int limit) {
	
	int offset=0;
	boolean sortOrder=true;
	HashSet <Key<PhysicalBookManifestation>> books = new HashSet<Key<PhysicalBookManifestation>>();;
	System.out.println("limit in keysByNurcode: "+limit);
	if (isRangeArgument(tuple.getValue()) == true) {
		String[] strings = tuple.getValue().split("-");
		int code_lower = new Integer(strings[0]).intValue();
		int code_upper = new Integer(strings[1]).intValue();
		books = persistentServiceImpl.getBookKeysByNurcode(code_lower, code_upper, offset, limit, sortOrder);
	} else if (isSingleIntValueArgument(tuple.getValue()) == true) {
		int code = new Integer(tuple.getValue()).intValue();
		books = persistentServiceImpl.getBookKeysByNurcode(code, offset, limit, sortOrder);
	}
			//getBookKeysByContributor(tuple.getValue(), tuple.getFlags());
	HashSet <Key<PhysicalBookManifestation>> returnSet = new HashSet <Key<PhysicalBookManifestation>>();
	for (Key<PhysicalBookManifestation> k : books) {
		returnSet.add(k);
	}
	return returnSet;
}
private HashSet <Key<PhysicalBookManifestation>> keysByRecent(SearchParametersTuple tuple) {
	if (tuple.getValue() == null) return null;
	return null;

}


private String returnResultAsOnix(String recordReference) {
	DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	Document doc = null;
	try {
		doc = factory.newDocumentBuilder().newDocument();
	} catch (Exception e) {
		System.err.println("Exception");
		e.printStackTrace();

		System.err.println(e.getMessage());
		System.err.println(e.getStackTrace());
		throw(new ResourceException(new Status(501), "RestBooksResource failed to allocate a Document resource, critical error."));
	}
	 
	//PrintWriter out = response.getWriter();
	//response.setContentType("text/xml");
	
	String st = recordReference;
	// check for length of ISBN
	if (st.length() != 13) {
		throw(new ResourceException(new Status(400), "ISBN is not syntactically correct"));
	} 
	// check for all digits of ISBN
	for (int i=0; i<st.length(); i++) {
		if (!Character.isDigit(st.charAt(i))) {
			throw(new ResourceException(new Status(400), "ISBN is not syntactically correct"));
		}
	}
	String isbnString = st.substring(0, 12);


	List<PhysicalBookManifestation> books = persistentServiceImpl.getBooksByISBN(isbnString);
	if (books.size() > 0) {
		PhysicalBookManifestation theBook = books.get(0);

		// 	encode the message
		DocumentFragment onixMessage = OnixUtility.getOnixMessage();
		if (onixMessage == null) {
			System.err.println("fragment is null");
			return "fragment is null";
		}
		doc.appendChild(doc.importNode(onixMessage, true));
		if (onixMessage.getOwnerDocument() == null) {
			System.err.println("owner document is null");
			return "owner document is null";
		}
		// add header
		DocumentFragment header = OnixUtility.getOnixMessageHeader();
		doc.getDocumentElement().appendChild(doc.importNode(header, true));
		
		// add product node for first product
		DocumentFragment product = OnixUtility.encodeToOnix(theBook);
		doc.getDocumentElement().appendChild(doc.importNode(product, true));
		/*
		System.out.println("Built an XML docFragment with "+onixMessage.getChildNodes().getLength()+" nodes");
		System.out.println("Nodes: ");
		for (int i = 0; i<onixMessage.getChildNodes().getLength(); i++) {
			System.out.println("node "+i+": "+onixMessage.getChildNodes().item(i).getNodeName());
			System.out.println("      has : "+onixMessage.getChildNodes().item(i).getChildNodes().getLength()+" children");
		}
		*/

		// transform and output the book
		Transformer transformer;
		try {
			// see: http://comments.gmane.org/gmane.org.google.gwt/45531
			System.setProperty("javax.xml.transform.TransformerFactory", "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
			transformer = TransformerFactory.newInstance().newTransformer();
			//transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			//javax.xml.transform.Source src = new DOMSource(docFragment.getOwnerDocument());
			//if (src == null) out.println("src is null");
			java.io.StringWriter writer = new StringWriter();
			javax.xml.transform.stream.StreamResult result = new javax.xml.transform.stream.StreamResult(writer);
			DOMSource source = new DOMSource(doc);
			//out.println("Got an XML; root node has  "+doc.getDocumentElement().getChildNodes().getLength()+" nodes");
			//out.println("First node under rootnode: "+doc.getDocumentElement().getChildNodes().item(0).getNodeName());
			transformer.transform(source, result);
			String xmlString = result.getWriter().toString();
			if (xmlString == null) System.out.println("xmlString is null");
			System.err.println("xmlString: " + xmlString);
			return(xmlString);
		} catch (Exception e) {
			System.err.println("Exception thrown: " + e.getMessage());
			System.out.print("Exception thrown: " + e.getMessage());
			e.printStackTrace();
		}

		
	} else {
		throw(new ResourceException(new Status(404), "Book not found."));
	}
	throw(new ResourceException(new Status(501), "RestBookResource finished without any meaningful status, critical error."));

}
}
